use std::{
    fs,
    path::{Path, PathBuf},
};

use argh::FromArgs;

use stencil_backend_python::PythonCg;
use stencil_codegen::{CodeBuffer, CodegenBackend, CodegenId};
use stencil_mir::MirModule;
use stencil_yaast::YaastModule;

#[derive(FromArgs)]
#[argh(description = "stencil code generator")]
struct Args {
    #[argh(option, description = "code generator to use (default infer)")]
    codegen: Option<String>,

    #[argh(positional, description = "source path")]
    source: PathBuf,

    #[argh(positional, description = "destination path (default stdout)")]
    dest: Option<PathBuf>,
}

fn main() {
    let args = argh::from_env::<Args>();
    if let Err(e) = main_inner(&args) {
        eprintln!("internal compiler error: {e}");
    }
}

fn main_inner(args: &Args) -> anyhow::Result<()> {
    // Decide which code generator to use.
    let codegen = match (args.codegen.as_ref(), args.dest.as_ref()) {
        (Some(cg_str), _) => CodegenId::from_name_str(cg_str),
        (None, Some(dest_path)) => dest_path.extension().and_then(CodegenId::from_ext),
        _ => None,
    };

    let Some(cg) = codegen else {
        anyhow::bail!("unable to decide code generator, specify one with --codegen or by putting a dest path with extension");
    };

    eprintln!("using codegen backend: {cg:?}");

    // Load and parse the yaast file.
    let yaast = load_yaast(&args.source)?;
    let buf = run_compile(&yaast, cg)?;

    // If there's a file then write to it.
    match &args.dest {
        Some(path) => {
            if let Err(e) = fs::write(path, buf) {
                anyhow::bail!("error writing output: {e}");
            }
        }
        None => {
            println!("{buf}");
        }
    }

    Ok(())
}

fn load_yaast(path: &Path) -> anyhow::Result<YaastModule> {
    let src = match fs::read(path) {
        Ok(src) => src,
        Err(e) => {
            anyhow::bail!("error reading source file: {e}");
        }
    };

    let yass_mod = match serde_yaml::from_slice::<YaastModule>(&src) {
        Ok(ym) => ym,
        Err(e) => {
            anyhow::bail!("error parsing source file: {e}");
        }
    };

    Ok(yass_mod)
}

fn run_compile(yass_mod: &YaastModule, cg: CodegenId) -> anyhow::Result<String> {
    let mod_def = match stencil_yaast::convert_to_mod_layout(&yass_mod) {
        Ok(ml) => ml,
        Err(e) => {
            anyhow::bail!("error extracting module defs: {e}");
        }
    };

    // FIXME double `layout`
    let mod_layouts = match stencil_layout::gen_mod_layout_from_mod_defn(&mod_def) {
        Ok(ir) => ir,
        Err(e) => {
            anyhow::bail!("error converting defs to layouts: {e}");
        }
    };

    let ir_mod = match stencil_mir::gen_ir_from_layout(&mod_layouts, "fixmeunnamed") {
        Ok(m) => m,
        Err(e) => {
            anyhow::bail!("error generating IR from layouts: {e}");
        }
    };

    let buf = dispatch_codegen_backend(&ir_mod, cg)?;

    Ok(buf)
}

fn dispatch_codegen_backend(mir_mod: &MirModule, codegen_id: CodegenId) -> anyhow::Result<String> {
    match codegen_id {
        CodegenId::Python => execute_backend(mir_mod, &PythonCg),
    }
}

fn execute_backend(mir_mod: &MirModule, inst: &impl CodegenBackend) -> anyhow::Result<String> {
    // Do the stage to lower the code to the lang IR.
    let lir_mod = inst.convert_mir_to_lir(&mir_mod)?;

    // TODO this is where we'd support late instrumentation passes or whatever

    // Then actually emit code into a buffer.
    let mut cb = CodeBuffer::new(inst.get_indent());
    inst.generate_lang_ir(&lir_mod, &mut cb)?;

    // Finish the codegen.
    let buf = cb.finalize();
    Ok(buf)
}
